{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# 7.验证码的识别处理\n",
    "目前，许多网站采取各种各样的措施来反爬虫，\n",
    "其中一个措施便是使用验证码。 随着技术的发展，\n",
    "验证码的花样越来越多。\n",
    "验证码最初是几个数字组合的简单的图形验证码，\n",
    "后来加入了英文字母和混淆曲线。\n",
    "有的网站还可能看到中文字符的验证码，这使得识别愈发困难。\n",
    "\n",
    "后来 12306 验证码的归现使得行为验证码开始发展起来，\n",
    "用过 12306 的用户肯定多少为它的验证码头疼过。\n",
    "我们需要识别文字，点击与文字描述相符的图片，验证码完全正确，才能通过。\n",
    "现在这种交互式验证码越来越多，\n",
    "如极验滑动验证码需要滑动拼合滑块才可以完成验证，\n",
    "点触验证码需要完全点击正确结果才可以完成验证，\n",
    "另外还有滑动宫格验证码、计算题验证码等。\n",
    "\n",
    "本章涉及的验证码有普通图形验证码、\n",
    "极验滑动验证码、点触验证码、\n",
    "微博宫格验证码，\n",
    "这些验证码识别的方式和思路各有不同。\n",
    "了解这几个验证码的识别方式之后，我们可以举一反三，\n",
    "用类似的方法识别其他类型验证码。\n",
    "\n",
    "## 1.图形验证码处理 —— use tesseract-ocr with tesserocr库 or pytesseract库\n",
    "\n",
    "以知网的验证码为例，讲解利用OCR技术识别图形验证码的方法\n",
    "\n",
    "第三方库：tesserocr/pytesseract\n",
    "\n",
    "项目：将验证码图片放到项目根目录下，用 tesserocr库识别该验证码，\n",
    "\n",
    "![](CheckCode.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "nigh.\n"
     ]
    }
   ],
   "source": [
    "from PIL import Image\n",
    "import pytesseract\n",
    "img = Image.open(\"CheckCode.png\")\n",
    "result = pytesseract.image_to_string(img)\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "识别和实际结果有偏差，\n",
    "这是因为验证码内的多余线条干扰了图片的识别。\n",
    "\n",
    "对于这种情况，我们还需要做一下额外的处理，如转灰度、二值化等操作。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "nigh.\n",
      "二值化后的验证码： nigh\n"
     ]
    }
   ],
   "source": [
    "from PIL import Image\n",
    "import pytesseract\n",
    "img = Image.open(\"CheckCode.png\").convert(\"L\")\n",
    "result = pytesseract.image_to_string(img)\n",
    "print(result)\n",
    "thre = 129\n",
    "table = []\n",
    "for i in range(256):\n",
    "    table.append(0) if i<thre else table.append(1)\n",
    "image = img.point(table,'1')\n",
    "image.show()\n",
    "res = pytesseract.image_to_string(image)\n",
    "print(\"二值化后的验证码：\",res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## 项目：结合wxPython实现一个比较复杂的识别系统\n",
    "\n",
    "项目文件：OcrScanning.py文件，打包后是OcrScanning.exe文件，\n",
    "要求有多国语言，有类似记事本的功能，可以写入文件，打开文件等等方式\n",
    "菜单等等都要具备\n",
    "\n",
    "## 2.极验滑动验证码的识别\n",
    "\n",
    "近几年出现了一些新型验证码，\n",
    "其巾比较有代表性的就是极验验证码，\n",
    "它需要拖动拼合滑块才可以完成验证，\n",
    "相对图形验证码来说识别难度上升了几个等级。\n",
    "本节将讲解极验验证码的识别过程。\n",
    "\n",
    "目标：我们的目标是用程序来识别并通过极验验证码的验证，包括分析识别思路、\n",
    "识别缺口位置、生成滑块拖动路径、\n",
    "模拟实现滑块拼合通过验证等步骤。\n",
    "\n",
    "使用工具：selenium+Chrome WebDriver\n",
    "\n",
    "极验验证码官网为：[http://www.geetest.com/](http://www.geetest.com/)。\n",
    "它是一个专注于提供验证安全的系统，\n",
    "主要验证方式是拖动滑块拼合图像。\n",
    "若图像完全拼合，则验证成功，即表单成功提交，\n",
    "否则需要重新验证.\n",
    "\n",
    "极验验证码相较于图形验证码来说识别难度更大。\n",
    "对于极验验证码 3.0 版本，我们首先点击按钮进行智能验证。\n",
    "如果验证不通过，则会弹出滑动验证的窗口，\n",
    "拖动滑块拼合图像进行验证。\n",
    "之后三个加密参数会生成，通过表单提交到后台，\n",
    "后台还会进行一次验证。\n",
    "\n",
    "全平台兼容，适用各种交互场景。\n",
    "极验验证码兼容所有主流浏览器甚至于IE6，\n",
    "也可以轻松应用在iOS和 Android移动端平台，满足各种业务需求\n",
    "\n",
    "识别：\n",
    "对于应用了极验验证码的网站如果我们直接模拟表单提交，\n",
    "加密参数的构造是个问题，需要分析其加密和校验逻辑，\n",
    "相对烦琐。所以我们采用直接模拟浏览器动作的方式来完成验证。\n",
    "在Python中，我们可以使用Selenium来完全模拟人的行为的方式来完成验证，\n",
    "此验证成本相比直接去识别加密算法少很多。\n",
    "\n",
    "首先找到一个带有极验验证的网站，如极验官方后台，链接为[https://account.geetest.com/login](https://account.geetest.com/login)\n",
    "在登录按钮上方有一个极验验证按钮\n",
    "此按钮为智能验证按钮。\n",
    "一般来说，如果是同一个会话，一段时间内第二次点击会直接通过验证。\n",
    "如果智能识别不通过，则会弹出滑动验证窗口，\n",
    "我们要拖动滑块拼合图像完成二步验证，\n",
    "\n",
    "综上，所以，识别验证需要完成如下三步。\n",
    "(1)模拟点击验证按钮。\n",
    "(2)识别滑动缺口的位置。\n",
    "(3)模拟拖动滑块\n",
    "\n",
    "第（1）步操作最简单，我们可以直接用 Selenium模拟点击按钮。\n",
    "\n",
    "第（2）步操作识别缺口的位置比较关键，这里需要用到图像的相关处理方法。首先观察缺口的样子\n",
    "\n",
    "缺口的四周边缘有明显的断裂边缘，\n",
    "边缘和边缘周围有明显的区别。\n",
    "我们可以实现一个边缘检测算法来找出缺口的位置。\n",
    "对于极验验证码来说，\n",
    "我们可以利用和原图对比检测的方式来识别缺口的位置，\n",
    "因为在没有滑动滑块之前，\n",
    "缺口并没有呈现。\n",
    "\n",
    "![](captcha1.png)   ![](captcha2.png)\n",
    "\n",
    "可以同时获取两张图片。\n",
    "设定一个对比阔值，然后遍历两张图片，\n",
    "找出相同位置像素RGB差距超过此阔值的像素点，\n",
    "那么此像素点的位置就是缺口的位置。\n",
    "\n",
    "首先我们需要注册一个账号。\n",
    "### 初始化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "#内存中，开辟的一个二进制模式的buffer，可以像文件对象一样操作它\n",
    "import time\n",
    "from io import BytesIO\n",
    "from PIL import Image\n",
    "from selenium import webdriver\n",
    "from selenium.webdriver import ActionChains\n",
    "from selenium.webdriver.common.by import By\n",
    "from selenium.webdriver.support.ui import WebDriverWait\n",
    "from selenium.webdriver.support import expected_conditions as EC\n",
    "\n",
    "EMAIL = ''\n",
    "PASSWORD = ''\n",
    "BORDER = 6\n",
    "INIT_LEFT = 60\n",
    "\n",
    "\n",
    "class CrackGeetest():\n",
    "    def __init__(self):\n",
    "        self.url = 'https://account.geetest.com/login'\n",
    "        self.browser = webdriver.Chrome()\n",
    "        self.wait = WebDriverWait(self.browser, 20)\n",
    "        self.email = EMAIL\n",
    "        self.password = PASSWORD\n",
    "\n",
    "    def __del__(self):\n",
    "        self.browser.close()\n",
    "\n",
    "    def get_geetest_button(self):\n",
    "        \"\"\"\n",
    "        获取初始验证按钮\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))\n",
    "        return button\n",
    "\n",
    "    def get_position(self):\n",
    "        \"\"\"\n",
    "        获取验证码位置\n",
    "        :return: 验证码位置元组\n",
    "        \"\"\"\n",
    "        img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))\n",
    "        time.sleep(2)\n",
    "        location = img.location\n",
    "        size = img.size\n",
    "        top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[\n",
    "            'width']\n",
    "        return (top, bottom, left, right)\n",
    "\n",
    "    def get_screenshot(self):\n",
    "        \"\"\"\n",
    "        获取网页截图\n",
    "        :return: 截图对象\n",
    "        \"\"\"\n",
    "        screenshot = self.browser.get_screenshot_as_png()\n",
    "        screenshot = Image.open(BytesIO(screenshot))\n",
    "        return screenshot\n",
    "\n",
    "    def get_slider(self):\n",
    "        \"\"\"\n",
    "        获取滑块\n",
    "        :return: 滑块对象\n",
    "        \"\"\"\n",
    "        slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))\n",
    "        return slider\n",
    "\n",
    "    def get_geetest_image(self, name='captcha.png'):\n",
    "        \"\"\"\n",
    "        获取验证码图片\n",
    "        :return: 图片对象\n",
    "        \"\"\"\n",
    "        top, bottom, left, right = self.get_position()\n",
    "        print('验证码位置', top, bottom, left, right)\n",
    "        screenshot = self.get_screenshot()\n",
    "        captcha = screenshot.crop((left, top, right, bottom))\n",
    "        captcha.save(name)\n",
    "        return captcha\n",
    "\n",
    "    def open(self):\n",
    "        \"\"\"\n",
    "        打开网页输入用户名密码\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        self.browser.get(self.url)\n",
    "        email = self.wait.until(EC.presence_of_element_located((By.ID, 'email')))\n",
    "        password = self.wait.until(EC.presence_of_element_located((By.ID, 'password')))\n",
    "        email.send_keys(self.email)\n",
    "        password.send_keys(self.password)\n",
    "\n",
    "    def get_gap(self, image1, image2):\n",
    "        \"\"\"\n",
    "        获取缺口偏移量\n",
    "        :param image1: 不带缺口图片\n",
    "        :param image2: 带缺口图片\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        left = 60\n",
    "        for i in range(left, image1.size[0]):\n",
    "            for j in range(image1.size[1]):\n",
    "                if not self.is_pixel_equal(image1, image2, i, j):\n",
    "                    left = i\n",
    "                    return left\n",
    "        return left\n",
    "\n",
    "    def is_pixel_equal(self, image1, image2, x, y):\n",
    "        \"\"\"\n",
    "        判断两个像素是否相同\n",
    "        :param image1: 图片1\n",
    "        :param image2: 图片2\n",
    "        :param x: 位置x\n",
    "        :param y: 位置y\n",
    "        :return: 像素是否相同\n",
    "        \"\"\"\n",
    "        # 取两个图片的像素点\n",
    "        pixel1 = image1.load()[x, y]\n",
    "        pixel2 = image2.load()[x, y]\n",
    "        threshold = 60\n",
    "        if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(\n",
    "                pixel1[2] - pixel2[2]) < threshold:\n",
    "            return True\n",
    "        else:\n",
    "            return False\n",
    "\n",
    "    def get_track(self, distance):\n",
    "        \"\"\"\n",
    "        根据偏移量获取移动轨迹\n",
    "        :param distance: 偏移量\n",
    "        :return: 移动轨迹\n",
    "        \"\"\"\n",
    "        # 移动轨迹\n",
    "        track = []\n",
    "        # 当前位移\n",
    "        current = 0\n",
    "        # 减速阈值\n",
    "        mid = distance * 4 / 5\n",
    "        # 计算间隔\n",
    "        t = 0.2\n",
    "        # 初速度\n",
    "        v = 0\n",
    "\n",
    "        while current < distance:\n",
    "            if current < mid:\n",
    "                # 加速度为正2\n",
    "                a = 2\n",
    "            else:\n",
    "                # 加速度为负3\n",
    "                a = -3\n",
    "            # 初速度v0\n",
    "            v0 = v\n",
    "            # 当前速度v = v0 + at\n",
    "            v = v0 + a * t\n",
    "            # 移动距离x = v0t + 1/2 * a * t^2\n",
    "            move = v0 * t + 1 / 2 * a * t * t\n",
    "            # 当前位移\n",
    "            current += move\n",
    "            # 加入轨迹\n",
    "            track.append(round(move))\n",
    "        return track\n",
    "\n",
    "    def move_to_gap(self, slider, track):\n",
    "        \"\"\"\n",
    "        拖动滑块到缺口处\n",
    "        :param slider: 滑块\n",
    "        :param track: 轨迹\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        ActionChains(self.browser).click_and_hold(slider).perform()\n",
    "        for x in track:\n",
    "            ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()\n",
    "        time.sleep(0.5)\n",
    "        ActionChains(self.browser).release().perform()\n",
    "\n",
    "    def login(self):\n",
    "        \"\"\"\n",
    "        登录\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        submit = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'login-btn')))\n",
    "        submit.click()\n",
    "        time.sleep(10)\n",
    "        print('登录成功')\n",
    "\n",
    "    def crack(self):\n",
    "        # 输入用户名密码\n",
    "        self.open()\n",
    "        # 点击验证按钮\n",
    "        button = self.get_geetest_button()\n",
    "        button.click()\n",
    "        # 获取验证码图片\n",
    "        image1 = self.get_geetest_image('captcha1.png')\n",
    "        # 点按呼出缺口\n",
    "        slider = self.get_slider()\n",
    "        slider.click()\n",
    "        # 获取带缺口的验证码图片\n",
    "        image2 = self.get_geetest_image('captcha2.png')\n",
    "        # 获取缺口位置\n",
    "        gap = self.get_gap(image1, image2)\n",
    "        print('缺口位置', gap)\n",
    "        # 减去缺口位移\n",
    "        gap -= BORDER\n",
    "        # 获取移动轨迹\n",
    "        track = self.get_track(gap)\n",
    "        print('滑动轨迹', track)\n",
    "        # 拖动滑块\n",
    "        self.move_to_gap(slider, track)\n",
    "\n",
    "        success = self.wait.until(\n",
    "            EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_success_radar_tip_content'), '验证成功'))\n",
    "        print(success)\n",
    "\n",
    "        # 失败后重试\n",
    "        if not success:\n",
    "            self.crack()\n",
    "        else:\n",
    "            self.login()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    crack = CrackGeetest()\n",
    "    crack.crack()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#说明：\n",
    "\n",
    "识别缺口的位置：首先获取前后两张比对图片，二者不一致的地方即为缺口。\n",
    "获取不带缺口的图片，利用Selenium选取图片元素，\n",
    "得到其所在位置和宽高，然后获取整个网页的截图，\n",
    "图片裁切出来即可\n",
    "\n",
    "\n",
    "\n",
    "## 3.点触验证码识别技术\n",
    "\n",
    "除了极验验证码，还有另一种常见且应用广泛的验证码，即点触验证码。\n",
    "\n",
    "样例：![](Clickcheck.png)\n",
    "\n",
    "直接点击图中符合要求的图。所有答案均正确，验证才会成功。\n",
    "如果有一个答案错误，验证就会失败。这种验证码就称为点触验证码。\n",
    "\n",
    "如果依靠图像识别点触验证码，则识别难度非常大。\n",
    "\n",
    "相关字体经过变形、放缩、模糊处理，如果要借助前面的OCR技术来识别，\n",
    "识别的精准度会大打折扣，甚至得不到任何结果。因此不能实现。\n",
    "\n",
    "此外我们也需要将图像重新转化文字，\n",
    "可以借助各种识图接口，但识别的准确率非常低，\n",
    "经常会出现匹配不正确或无法匹配的情况。\n",
    "而且图片清晰度不够，\n",
    "识别难度会更大，更何况有时需要同时正确识别多张图片，验证才能通过。\n",
    "\n",
    "### 那么，此类验证码该如何识别？\n",
    "\n",
    "互联网上有很多验证码服务平台，这些平台 7×24 小时提供验证码识别服务，\n",
    "一张图片几秒就会获得识别结果，准确率可达90%以上。\n",
    "\n",
    "这里推荐的是超级鹰，官网为[https://www.chaojiying.com](https://www.chaojiying.com)。\n",
    "其提供的服务种类非常广泛，可识别的验证码类型非常多，\n",
    "其中就包括点触验证码等等。\n",
    "\n",
    "超级鹰平台同样支持简单的图形，验证码识别。\n",
    "如果OCR识别有难度，\n",
    "同样可以用本节介绍的方法借助此平台来识别。\n",
    "\n",
    "具体如有变动以官网为准：\n",
    "[https://www.chaojiying.com/price.html](https://www.chaojiying.com/price.html)\n",
    "\n",
    "### 流程：\n",
    "\n",
    "例如：项目要求是点击相关的图片的文字实现识别，如找到图片中的某某字体等等\n",
    "\n",
    "1、先注册超级鹰账号井申请软件ID，\n",
    "注册页面链接为\n",
    "[https://www.chaojiying.com/user/reg/](https://www.chaojiying.com/user/reg/)\n",
    "在后台开发商中心添加软件ID。\n",
    "最后充值一些题分，充值多少可以根据价格和识别量自行决定。\n",
    "\n",
    "2、获取API\n",
    "\n",
    "在官方网站下载对应的Python API，链接为：https://www.chaojiying.com/api-14.html。\n",
    "此API是Python2 版本的，是用requests库来实现的。\n",
    "我们可以简单更改几个地方，即可将其修改为Python3版本。\n",
    "\n",
    "修改后的相关信息如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "#!/usr/bin/env python\n",
    "# coding:utf-8\n",
    "import requests\n",
    "from hashlib import md5\n",
    "\n",
    "class Chaojiying_Client(object):\n",
    "    def __init__(self, username, password, soft_id):\n",
    "        self.username = username\n",
    "        password =  password.encode('utf8')\n",
    "        self.password = md5(password).hexdigest()\n",
    "        self.soft_id = soft_id\n",
    "        self.base_params = {\n",
    "            'user': self.username,\n",
    "            'pass2': self.password,\n",
    "            'softid': self.soft_id,\n",
    "        }\n",
    "        self.headers = {\n",
    "            'Connection': 'Keep-Alive',\n",
    "            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',\n",
    "        }\n",
    "\n",
    "    def PostPic(self, im, codetype):\n",
    "        \"\"\"\n",
    "        im: 图片字节\n",
    "        codetype: 题目类型 参考 http://www.chaojiying.com/price.html\n",
    "        \"\"\"\n",
    "        params = {\n",
    "            'codetype': codetype,\n",
    "        }\n",
    "        params.update(self.base_params)\n",
    "        files = {'userfile': ('ccc.jpg', im)}\n",
    "        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)\n",
    "        return r.json()\n",
    "\n",
    "    def ReportError(self, im_id):\n",
    "        \"\"\"\n",
    "        im_id:报错题目的图片ID\n",
    "        \"\"\"\n",
    "        params = {\n",
    "            'id': im_id,\n",
    "        }\n",
    "        params.update(self.base_params)\n",
    "        r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)\n",
    "        return r.json()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001') #用户中心>>软件ID 生成一个替换 96001\n",
    "    im = open('a.jpg', 'rb').read()                          #本地图片文件路径 来替换 a.jpg 有时WIN系统须要//\n",
    "    print(chaojiying.PostPic(im, 1902))                      #1902 验证码类型  官方网站>>价格体系"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import time\n",
    "from io import BytesIO\n",
    "from PIL import Image\n",
    "from selenium import webdriver\n",
    "from selenium.webdriver import ActionChains\n",
    "from selenium.webdriver.common.by import By\n",
    "from selenium.webdriver.support.ui import WebDriverWait\n",
    "from selenium.webdriver.support import expected_conditions as EC\n",
    "from chaojiying import Chaojiying\n",
    "\n",
    "EMAIL = 'cqc@cuiqingcai.com'\n",
    "PASSWORD = ''\n",
    "\n",
    "CHAOJIYING_USERNAME = 'Germey'\n",
    "CHAOJIYING_PASSWORD = ''\n",
    "CHAOJIYING_SOFT_ID = 893590\n",
    "CHAOJIYING_KIND = 9102\n",
    "\n",
    "\n",
    "class CrackTouClick():\n",
    "    def __init__(self):\n",
    "        self.url = 'http://admin.touclick.com/login.html'\n",
    "        self.browser = webdriver.Chrome()\n",
    "        self.wait = WebDriverWait(self.browser, 20)\n",
    "        self.email = EMAIL\n",
    "        self.password = PASSWORD\n",
    "        self.chaojiying = Chaojiying(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)\n",
    "\n",
    "    def __del__(self):\n",
    "        self.browser.close()\n",
    "\n",
    "    def open(self):\n",
    "        \"\"\"\n",
    "        打开网页输入用户名密码\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        self.browser.get(self.url)\n",
    "        email = self.wait.until(EC.presence_of_element_located((By.ID, 'email')))\n",
    "        password = self.wait.until(EC.presence_of_element_located((By.ID, 'password')))\n",
    "        email.send_keys(self.email)\n",
    "        password.send_keys(self.password)\n",
    "\n",
    "    def get_touclick_button(self):\n",
    "        \"\"\"\n",
    "        获取初始验证按钮\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'touclick-hod-wrap')))\n",
    "        return button\n",
    "\n",
    "    def get_touclick_element(self):\n",
    "        \"\"\"\n",
    "        获取验证图片对象\n",
    "        :return: 图片对象\n",
    "        \"\"\"\n",
    "        element = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'touclick-pub-content')))\n",
    "        return element\n",
    "\n",
    "    def get_position(self):\n",
    "        \"\"\"\n",
    "        获取验证码位置\n",
    "        :return: 验证码位置元组\n",
    "        \"\"\"\n",
    "        element = self.get_touclick_element()\n",
    "        time.sleep(2)\n",
    "        location = element.location\n",
    "        size = element.size\n",
    "        top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[\n",
    "            'width']\n",
    "        return (top, bottom, left, right)\n",
    "\n",
    "    def get_screenshot(self):\n",
    "        \"\"\"\n",
    "        获取网页截图\n",
    "        :return: 截图对象\n",
    "        \"\"\"\n",
    "        screenshot = self.browser.get_screenshot_as_png()\n",
    "        screenshot = Image.open(BytesIO(screenshot))\n",
    "        return screenshot\n",
    "\n",
    "    def get_touclick_image(self, name='captcha.png'):\n",
    "        \"\"\"\n",
    "        获取验证码图片\n",
    "        :return: 图片对象\n",
    "        \"\"\"\n",
    "        top, bottom, left, right = self.get_position()\n",
    "        print('验证码位置', top, bottom, left, right)\n",
    "        screenshot = self.get_screenshot()\n",
    "        captcha = screenshot.crop((left, top, right, bottom))\n",
    "        captcha.save(name)\n",
    "        return captcha\n",
    "\n",
    "    def get_points(self, captcha_result):\n",
    "        \"\"\"\n",
    "        解析识别结果\n",
    "        :param captcha_result: 识别结果\n",
    "        :return: 转化后的结果\n",
    "        \"\"\"\n",
    "        groups = captcha_result.get('pic_str').split('|')\n",
    "        locations = [[int(number) for number in group.split(',')] for group in groups]\n",
    "        return locations\n",
    "\n",
    "    def touch_click_words(self, locations):\n",
    "        \"\"\"\n",
    "        点击验证图片\n",
    "        :param locations: 点击位置\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        for location in locations:\n",
    "            print(location)\n",
    "            ActionChains(self.browser).move_to_element_with_offset(self.get_touclick_element(), location[0],\n",
    "                                                                   location[1]).click().perform()\n",
    "            time.sleep(1)\n",
    "\n",
    "    def touch_click_verify(self):\n",
    "        \"\"\"\n",
    "        点击验证按钮\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'touclick-pub-submit')))\n",
    "        button.click()\n",
    "\n",
    "    def login(self):\n",
    "        \"\"\"\n",
    "        登录\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        submit = self.wait.until(EC.element_to_be_clickable((By.ID, '_submit')))\n",
    "        submit.click()\n",
    "        time.sleep(10)\n",
    "        print('登录成功')\n",
    "\n",
    "    def crack(self):\n",
    "        \"\"\"\n",
    "        破解入口\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        self.open()\n",
    "        # 点击验证按钮\n",
    "        button = self.get_touclick_button()\n",
    "        button.click()\n",
    "        # 获取验证码图片\n",
    "        image = self.get_touclick_image()\n",
    "        bytes_array = BytesIO()\n",
    "        image.save(bytes_array, format='PNG')\n",
    "        # 识别验证码\n",
    "        result = self.chaojiying.post_pic(bytes_array.getvalue(), CHAOJIYING_KIND)\n",
    "        print(result)\n",
    "        locations = self.get_points(result)\n",
    "        self.touch_click_words(locations)\n",
    "        self.touch_click_verify()\n",
    "        # 判定是否成功\n",
    "        success = self.wait.until(\n",
    "            EC.text_to_be_present_in_element((By.CLASS_NAME, 'touclick-hod-note'), '验证成功'))\n",
    "        print(success)\n",
    "\n",
    "        # 失败后重试\n",
    "        if not success:\n",
    "            self.crack()\n",
    "        else:\n",
    "            self.login()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    crack = CrackTouClick()\n",
    "    crack.crack()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里定义了一个Chaojiying类，其构造函数接收三个参数，\n",
    "分别是超级鹰的用户名、密码以及软件ID，保存以备使用。\n",
    "\n",
    "最重要的一个方法叫作post_pic()，它需要传图片对象和验证码的代号。\n",
    "该方法会将图片对象 和相关信息发给超级鹰的后台进行识别，\n",
    "然后将识别成功的json返回。\n",
    "\n",
    "另一个方法叫作report_error()，它是发生错误的时候的回调。\n",
    "如果验证码识别错误，调用此方法会返回相应的题分。\n",
    "\n",
    "通过在线打码平台辅助完成了验证码的识别。\n",
    "这种识别方法非常强大，几乎任意的验证码都可以识别。\n",
    "如果遇到难题，借助打码平台无疑是一个极佳的选择。\n",
    "\n",
    "## 4.微博等宫格验证码的识别技术\n",
    "\n",
    "微博宫格验证码是一种新型交互式验证码，\n",
    "每个’白’ 格之间会有一条指示连线，\n",
    "指示了应该的滑动轨迹。\n",
    "我们要按照滑动轨迹依次从起始宫格滑动到终止宫格，\n",
    "才可以完成验证，\n",
    "\n",
    "访问新浪微博移动版登录页面，就可以看到如上验证码，\n",
    "链接为[https://passport.weibo.cn/signin/login](https://passport.weibo.cn/signin/login),\n",
    "不是每次登录都会出现验证码，当频繁登录或者账号存在安全风险的时候，验证码才会出现。\n",
    "\n",
    "4个点的连接方法有很多：\n",
    "识别从探寻规律入手。\n",
    "规律就是，此验证码的四个宫格一定是有连线经过的，\n",
    "每一条连线上都会 相应的指示箭头，连线的形状多样，\n",
    "包括C型、Z型、X型等，同一类型的连线轨迹是相同的，唯一不同的就是连线的方向\n",
    "\n",
    "如果要完全识别滑动宫格顺序，\n",
    "就需要具体识别出箭头的朝向。 而整个验证码箭头朝向一共有8种，\n",
    "而且会出现在不同的位置。\n",
    "如果要写一个箭头方向识别算法，\n",
    "需要考虑不同箭头所在的位置，找出各个位置箭头的像素点坐标，\n",
    "计算像素点变化规律，这个工作量就会变得比较大。\n",
    "\n",
    "这时我们可以考虑用模板匹配的方法，\n",
    "就是将一些识别目标提前保存并做好标记，这称作模板。\n",
    "这里将验证码图片做好拖动顺序的标记当做模板。\n",
    "对比要新识别的目标和每一个模板，如果找到匹配的模板，\n",
    "则就成功识别出要新识别的目标。 在图像识别中，\n",
    "模板匹配也是常用的方法，实现简单且易用性好。\n",
    "\n",
    "我们必须要收集到足够多的模板，模板匹配方法的效果才会好。\n",
    "而对于微博宫格验证码来说，宫格只有4个，\n",
    "验证码的样式最多 4×3×2×1=24种，\n",
    "则我们可以将所有模板都收集下来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import time\n",
    "from io import BytesIO\n",
    "from PIL import Image\n",
    "from selenium import webdriver\n",
    "from selenium.common.exceptions import TimeoutException\n",
    "from selenium.webdriver import ActionChains\n",
    "from selenium.webdriver.common.by import By\n",
    "from selenium.webdriver.support.ui import WebDriverWait\n",
    "from selenium.webdriver.support import expected_conditions as EC\n",
    "from os import listdir\n",
    "\n",
    "USERNAME = '15874295385'\n",
    "PASSWORD = 'fpdpvx119'\n",
    "\n",
    "TEMPLATES_FOLDER = 'templates/'\n",
    "\n",
    "\n",
    "class CrackWeiboSlide():\n",
    "    def __init__(self):\n",
    "        self.url = 'https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/'\n",
    "        self.browser = webdriver.Chrome()\n",
    "        self.wait = WebDriverWait(self.browser, 20)\n",
    "        self.username = USERNAME\n",
    "        self.password = PASSWORD\n",
    "\n",
    "    def __del__(self):\n",
    "        self.browser.close()\n",
    "\n",
    "    def open(self):\n",
    "        \"\"\"\n",
    "        打开网页输入用户名密码并点击\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        self.browser.get(self.url)\n",
    "        username = self.wait.until(EC.presence_of_element_located((By.ID, 'loginName')))\n",
    "        password = self.wait.until(EC.presence_of_element_located((By.ID, 'loginPassword')))\n",
    "        submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'loginAction')))#显式等待\n",
    "        username.send_keys(self.username)\n",
    "        password.send_keys(self.password)#递交相关参数\n",
    "        submit.click()\n",
    "\n",
    "    def get_position(self):\n",
    "        \"\"\"\n",
    "        获取验证码位置\n",
    "        :return: 验证码位置元组\n",
    "        \"\"\"\n",
    "        try:\n",
    "            img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'patt-shadow')))\n",
    "        except TimeoutException:\n",
    "            print('未出现验证码')\n",
    "            self.open()#重新执行操作，直到出现验证码\n",
    "        time.sleep(2)\n",
    "        location = img.location\n",
    "        size = img.size\n",
    "        top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[\n",
    "            'width']\n",
    "        return (top, bottom, left, right)#返回相关位置的数据\n",
    "\n",
    "    def get_screenshot(self):\n",
    "        \"\"\"\n",
    "        获取网页截图\n",
    "        :return: 截图对象\n",
    "        \"\"\"\n",
    "        screenshot = self.browser.get_screenshot_as_png()\n",
    "        screenshot = Image.open(BytesIO(screenshot))\n",
    "        return screenshot   #以流的形式打开图片的二进制文件\n",
    "\n",
    "    def get_image(self, name='captcha.png'):\n",
    "        \"\"\"\n",
    "        获取验证码图片\n",
    "        :return: 图片对象\n",
    "        \"\"\"\n",
    "        top, bottom, left, right = self.get_position()\n",
    "        print('验证码位置', top, bottom, left, right)\n",
    "        screenshot = self.get_screenshot()\n",
    "        captcha = screenshot.crop((left, top, right, bottom))\n",
    "        captcha.save(name)\n",
    "        return captcha    \n",
    "\n",
    "    def is_pixel_equal(self, image1, image2, x, y):\n",
    "        \"\"\"\n",
    "        判断两个像素是否相同\n",
    "        :param image1: 图片1\n",
    "        :param image2: 图片2\n",
    "        :param x: 位置x\n",
    "        :param y: 位置y\n",
    "        :return: 像素是否相同\n",
    "        \"\"\"\n",
    "        # 取两个图片的像素点\n",
    "        pixel1 = image1.load()[x, y]\n",
    "        pixel2 = image2.load()[x, y]\n",
    "        threshold = 20\n",
    "        if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(\n",
    "                pixel1[2] - pixel2[2]) < threshold:\n",
    "            return True\n",
    "        else:\n",
    "            return False\n",
    "\n",
    "    def same_image(self, image, template):\n",
    "        \"\"\"\n",
    "        识别相似验证码\n",
    "        :param image: 待识别验证码\n",
    "        :param template: 模板\n",
    "        :return:\n",
    "        在这里比对图片也利用了遍历像素的方法。same_image()方法接收两个参数， \n",
    "        image 为待检测的验证码图片对象， template 是模板对象。\n",
    "        由于二者大小是完全一致的，所以在这里我们遍历了图片的所有像素点。\n",
    "        比对二者同一位置的像素点，如果像素点相同，计数就加l。 \n",
    "        最后计算相同的像素点占总 像素的比例。如果该比例超过一定值，那就判定图片完全相同，则匹配成功。 \n",
    "        这里阈值设定为 0.99, 即如果二者有 0.99 以上的相似比，则代表匹配成功\n",
    "        \"\"\"\n",
    "        # 相似度阈值\n",
    "        threshold = 0.99\n",
    "        count = 0\n",
    "        for x in range(image.width):\n",
    "            for y in range(image.height):\n",
    "                # 判断像素是否相同\n",
    "                if self.is_pixel_equal(image, template, x, y):\n",
    "                    count += 1\n",
    "        result = float(count) / (image.width * image.height)\n",
    "        if result > threshold:\n",
    "            print('成功匹配')\n",
    "            return True\n",
    "        return False\n",
    "\n",
    "\n",
    "\n",
    "    def detect_image(self, image):\n",
    "        \"\"\"\n",
    "        匹配图片\n",
    "        :param image: 图片\n",
    "        :return: 拖动顺序\n",
    "        TEMPLATEA_FOLDER 就是模板所在的文件夹。 \n",
    "        这里通过listdir()方法获取所有模板的文件名称，\n",
    "        然后对其进行遍历，通过same_image()方法对验证码和模板进行比对。 \n",
    "        如果匹配成功，那么就将匹配到的模板文件名转换为列表\n",
    "        \"\"\"\n",
    "        for template_name in listdir(TEMPLATES_FOLDER):\n",
    "            print('正在匹配', template_name)\n",
    "            template = Image.open(TEMPLATES_FOLDER + template_name)\n",
    "            if self.same_image(image, template):\n",
    "                # 返回顺序\n",
    "                numbers = [int(number) for number in list(template_name.split('.')[0])]\n",
    "                print('拖动顺序', numbers)\n",
    "                return numbers\n",
    "\n",
    "    def move(self, numbers):\n",
    "        \"\"\"\n",
    "        根据顺序拖动\n",
    "        :param numbers:\n",
    "        :return:\n",
    "        这里方法接收的参数就是宫格的点按顺序，如［3,1,2,4］。 \n",
    "        首先我们利用 find_elements_by_css_selector()方法获取到 4 个宫格元素，\n",
    "        它是一个列表形式，每个元素代表一个宫格。 \n",
    "        接下来遍历宫格的点按顺序，做一系列对应操作。 \n",
    "        其中如果当前遍历的是第一个宫格，那就直接鼠标点击并保持动作，否则移动到下一个宫格。 \n",
    "        如当前遍历的是最后一个宫格，那就松开鼠标，如果不是最后一个宫格， 则计算移动到下一个宫格的偏移量。 \n",
    "        通过4次循环，我们便可以成功操作浏览器完成宫格验证码的拖拽填充，松开鼠标之后即可识别成功\n",
    "        \"\"\"\n",
    "        # 获得四个按点\n",
    "        circles = self.browser.find_elements_by_css_selector('.patt-wrap .patt-circ')\n",
    "        dx = dy = 0\n",
    "        for index in range(4):\n",
    "            circle = circles[numbers[index] - 1]\n",
    "            # 如果是第一次循环\n",
    "            if index == 0:\n",
    "                # 点击第一个按点\n",
    "                ActionChains(self.browser) \\\n",
    "                    .move_to_element_with_offset(circle, circle.size['width'] / 2, circle.size['height'] / 2) \\\n",
    "                    .click_and_hold().perform()\n",
    "            else:\n",
    "                # 小幅移动次数\n",
    "                times = 30\n",
    "                # 拖动\n",
    "                for i in range(times):\n",
    "                    ActionChains(self.browser).move_by_offset(dx / times, dy / times).perform()\n",
    "                    time.sleep(1 / times)\n",
    "            # 如果是最后一次循环\n",
    "            if index == 3:\n",
    "                # 松开鼠标\n",
    "                ActionChains(self.browser).release().perform()\n",
    "            else:\n",
    "                # 计算下一次偏移\n",
    "                dx = circles[numbers[index + 1] - 1].location['x'] - circle.location['x']\n",
    "                dy = circles[numbers[index + 1] - 1].location['y'] - circle.location['y']\n",
    "    def crack(self):\n",
    "        \"\"\"\n",
    "        破解入口\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        self.open()\n",
    "        # 获取验证码图片\n",
    "        image = self.get_image('captcha.png')\n",
    "        numbers = self.detect_image(image)\n",
    "        self.move(numbers)\n",
    "        time.sleep(10)\n",
    "        print('识别结束')\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    crack = CrackWeiboSlide()\n",
    "    crack.crack()\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}